/* -*- mode: java; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*- */
/* vi: set ts=4 sw=4 expandtab: (add to ~/.vimrc: set modeline modelines=5) */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
 *
 * Generate selftest C++ code from spec files.
 *
 * Usage
 *
 *   selftest.abc filename ...
 *
 * Overview
 *
 *   The input to this program is some set of file names.  These
 *   should have a .st extension and conform to the grammar below.
 *
 *   This program processes the input files and creates the
 *   corresponding .cpp files (same path and file name, just different
 *   extension).  These .cpp files should be included in the project.
 *
 *   Additionally, the program produces a single file "SelftestInit.cpp"
 *   in the program's working directory.  This will contain a single
 *   function, "SelftestRunner::createGeneratedSelftestClasses()", which
 *   is called on AVM startup.  This file too should be included in the
 *   project.
 *
 * Grammar
 *
 *   file      ::= component category ifdef* prefix? (decls | methods | prologue | epilogue | test)*
 *   component ::= SOL "%%component" ident EOL
 *   category  ::= SOL "%%category" ident EOL
 *   ifdef     ::= SOL ( "%%ifdef" | "%%ifndef" ) ident EOL
 *   prefix    ::= SOL "%%prefix" EOL text
 *   decls     ::= SOL "%%decls" EOL text
 *   methods   ::= SOL "%%methods" EOL text
 *   prologue  ::= SOL "%%prologue" EOL text
 *   epilogue  ::= SOL "%%epilogue" EOL text
 *   test      ::= SOL ( "%%test" | %%explicit ) ident EOL (text | verify)*
 *   verify    ::= SOL "%%verify" expr EOL
 *   expr      ::= text but not newline
 *   text      ::= arbitrary text not containing "%%" at SOL
 *   EOL       ::= newline
 *   SOL       ::= beginning of line, possibly with leading spaces
 *
 *   Comment lines are C++ style, "//" possibly preceded by blanks, to the end of line.
 *   Blank lines are legal everywhere.
 *
 * Semantic constraints
 *
 *   There can be an arbitrary number of tests, but at least one.
 *   There can be at most one prologue or epilogue.
 *   There can be an arbitrary number of decls and methods, they are just cat'ed together.
 *   There must be at least one verify in a test.
 *
 *   (The requirements for at least one test and at least one verify
 *   are fascistic, but may help catch errors.  Experience will tell.)
 *
 * Semantics
 *
 *   A selftest file defines one or more test cases for a component
 *   (like "avmplus" or "player") and a category (like "peephole" or
 *   "jit").  Additionally, each test is named.  The test harness lets
 *   the user define the components, categories, and tests to run.
 *
 *   Each generated file is compiled conditionally on the existence of
 *   VMCFG_SELFTEST and also each of the names declared in %%ifdef
 *   statements.
 *
 *   Code is generated into a "namespace avmplus" block.

 *   The %%prefix text, if any, is inserted verbatim before generated
 *   classes or methods; it is useful for "use namespace", class
 *   definitions, and so on.
 *
 *   Apart from the prefix, the code that is generated comprises a
 *   class and its methods. Text in %%decls blocks is inserted
 *   verbatim into the class definition; text in %%methods blocks is
 *   inserted verbatim into the top level of the file following the
 *   class definition.  These sections allow the test case to define
 *   and use auxiliary data and methods.
 *
 *   The code in a %%prologue section will be run once, before any
 *   test in the file is run.  The code in an %%epilogue section will
 *   also be run once, after all tests in the file have been run.
 *   They are typically used to initialize instance data (declared by
 *   %%decls).
 *
 *   Each %%test or %%explicit is enclosed in an anonymous method of
 *   the generated class.  They have the same meaning but if
 *   %%explicit is used then the test must be selected explicitly by
 *   component, category, and test.  Each %%verify that appears in the
 *   test will test its condition, and if it does not hold that test
 *   function will be aborted and the error recorded.  Subsequent
 *   %%verify statements in that test will not be executed; however,
 *   subsequent tests in the same selftest spec will be executed.
 *
 *   Comment lines preceding directives are copied verbatim to the output.
 */

package selftest
{
    import avmplus.*;

    class Selftest {
        var comments = [];
        var component = null;
        var category = null;
        var ifdefs = [];
        var ifndefs = [];
        var prefix = [];
        var decls = [];
        var methods = [];
        var prologue = null;
        var epilogue = null;
        var tests = [];
        // generated by the formatter
        var ifdef_text = null;
        var ifndef_text = null;
        var classname = null;
        var namespace_name = null;
    }

    function parse(lines, filename) {
        var st = new Selftest;
        var i=0;
        var l=lines.length;
        var res;
        var state = -1;
        while (i < l) {
            var line = lines[i++];
            if (res = /^\s*%%component\s+([a-zA-Z_][a-zA-Z0-9_]+)\s*$/.exec(line)) {
                if (state > 0)
                    throw "Too late to define component";
                state = 0;
                if (st.component != null)
                    throw "Component already defined";
                st.component = res[1];
            }
            else if (res = /^\s*%%category\s+([a-zA-Z_][a-zA-Z0-9_]+)\s*$/.exec(line)) {
                if (state > 0)
                    throw "Too late to define category";
                state = 0;
                if (st.category != null)
                    throw "Category already defined";
                st.category = res[1];
            }
            else if (res = /^\s*%%ifdef\s+([a-zA-Z_][a-zA-Z0-9_]+)\s*$/.exec(line)) {
                if (state > 1)
                    throw "Too late to define ifdef";
                state = 1;
                st.ifdefs.push(res[1]);
            }
            else if (res = /^\s*%%ifndef\s+([a-zA-Z_][a-zA-Z0-9_]+)\s*$/.exec(line)) {
                if (state > 1)
                    throw "Too late to define ifndef";
                state = 1;
                st.ifndefs.push(res[1]);
            }
            else if (line.match(/^\s*%%prefix/)) {
                if (state > 2)
                    throw "Too late to define prefix";
                state = 2;
                st.prefix = text();
                state = 3;
            }
            else if (line.match(/^\s*%%decls/)) {
                state = 3;
                pushMultiple(st.decls, text());
            }
            else if (line.match(/^\s*%%methods/)) {
                state = 3;
                pushMultiple(st.methods, text());
            }
            else if (line.match(/^\s*%%prologue/)) {
                state = 3;
                if (st.prologue != null)
                    throw "Prologue already defined";
                st.prologue = text();
            }
            else if (line.match(/^\s*%%epilogue/)) {
                state = 3;
                if (st.epilogue != null)
                    throw "Epilogue already defined";
                st.epilogue = text();
            }
            else if (res = /^\s*(%%test|%%explicit)\s+([a-zA-Z_][a-zA-Z0-9_]+)\s*$/.exec(line)) {
                state = 3;
                var loc = i;
                var t = [];
                var vs = 0;
                var name = res[2];
                var explicit = res[1] == "%%explicit";
                while (i < l) {
                    pushMultiple(t, text());
                    if (i == l)
                        break;
                    line = lines[i];
                    if ((res = /^\s*%%verify\s+(.*)$/.exec(line)) == null)
                        break;
                    vs++;
                    i++;
                    t.push("// line " + i + " \"" + filename + "\"");
                    t.push("verifyPass(" + res[1] + ", \"" + quote(res[1]) + "\", __FILE__, __LINE__);");
                }
                if (vs == 0)
                    throw "No %%verify statements for test on line " + loc;
                st.tests.push([name, explicit, t]);
            }
            else if (line.match(/^\s*\/\//)) {
                if (state < 0)
                    st.comments.push(line);
            }
            else if (line.match(/^\s*$/)) {
                if (state < 0)
                    st.comments.push(line);
            }
            else {
                throw "Bogus line " + (i+1) + ": " + line;
            }
        }

        if (st.category == null)
            throw "Category missing";
        if (st.component == null)
            throw "Component missing";
        if (st.tests.length == 0)
            throw "No tests";

        return st;

        // note, this uses i, l, and lines and updates i
        function text() {
            var res;
            var t = [];
            while (i < l) {
                line = lines[i];
                if (line.match(/^\s*%%/))
                    break;
                i++;
                t.push(line);
            }
            return t;
        }

        function quote(s) {
            return s.replace(/\"/g, "\\\"");
        }
    }

    function formatSelftest(input, st) {
        var s = [];
        var classname = "ST_" + st.component + "_" + st.category;
        var namespace_name = classname;
        st.classname = classname;
        st.namespace_name = namespace_name;
        s.push("// Generated from " + input);
        pushMultiple(s, st.comments);
        s.push("#include \"avmshell.h\"");
        st.ifdef_text = null;
        if (st.ifdefs.length > 0)
            st.ifdef_text = "#if defined " + st.ifdefs.join(" && defined ");
        if (st.ifndefs.length > 0)
            st.ifndef_text = "#if !defined " + st.ifndefs.join(" && !defined ");
        s.push("#ifdef VMCFG_SELFTEST");
        if (st.ifdef_text != null)
            s.push(st.ifdef_text);
        if (st.ifndef_text != null)
            s.push(st.ifndef_text);
        s.push("namespace avmplus {");
        s.push("namespace "+namespace_name+" {");
        for ( var i=0 ; i < st.prefix.length ; i++ )
            s.push(st.prefix[i]);

        s.push("class " + classname + " : public Selftest {");
        s.push("public:");
        s.push(classname + "(AvmCore* core);");
        s.push("virtual void run(int n);");
        if (st.prologue)
            s.push("virtual void prologue();");
        if (st.epilogue)
            s.push("virtual void epilogue();");
        s.push("private:");
        s.push("static const char* ST_names[];");
        s.push("static const bool ST_explicits[];");
        for ( var i=0 ; i < st.tests.length ; i++ )
            s.push("void test" + i + "();");
        pushMultiple(s, st.decls);
        s.push("};");

        s.push(classname + "::" + classname + "(AvmCore* core)");
        s.push("    : Selftest(core, \"" + st.component + "\", \"" + st.category + "\", " + classname + "::ST_names," + classname + "::ST_explicits)");
        s.push("{}");

        s.push("const char* " + classname + "::ST_names[] = {" +
               st.tests.map(function (t) { return '"' + t[0] + '"' }).join(",") + ", NULL };");

        s.push("const bool " + classname + "::ST_explicits[] = {" +
               st.tests.map(function (t) { return t[1] }).join(",") + ", false };");

        s.push("void " + classname + "::run(int n) {");
        s.push("switch(n) {");
        for ( var i=0 ; i < st.tests.length ; i++ )
            s.push("case " + i + ": test" + i + "(); return;");
        s.push("}");
        s.push("}");

        if (st.prologue) {
            s.push("void " + classname + "::prologue() {");
            pushMultiple(s, st.prologue);
            s.push("}");
        }

        if (st.epilogue) {
            s.push("void " + classname + "::epilogue() {");
            pushMultiple(s, st.epilogue);
            s.push("}");
        }

        if (st.methods) {
            pushMultiple(s, st.methods);
        }

        for ( var i=0 ; i < st.tests.length ; i++ ) {
            s.push("void " + classname + "::test" + i + "() {");
            pushMultiple(s, st.tests[i][2]);
            s.push("}");
        }

        s.push("void create_" + st.component + "_" + st.category + "(AvmCore* core) { new " + classname + "(core); }");

        s.push("}");
        s.push("}");
        if (st.ifndef_text != null)
            s.push("#endif");
        if (st.ifdef_text != null)
            s.push("#endif");
        s.push("#endif");

        return s.join("\n") + "\n";
    }

    function pushMultiple(stk, xs) {
        for ( var i=0 ; i < xs.length ; i++ )
            stk.push(xs[i]);
        return stk;
    }

    // VS2008 requires the extern declarations to be at the top level for
    // the namespace tagging to work out correctly.
    function formatGeneratedInitializer() {
        var s = [];
        s.push("/* This Source Code Form is subject to the terms of the Mozilla Public");
        s.push(" * License, v. 2.0. If a copy of the MPL was not distributed with this");
        s.push(" * file, You can obtain one at http://mozilla.org/MPL/2.0/. */");
        s.push("// Initialization code for generated selftest code");
        s.push("#include \"avmshell.h\"");
        s.push("namespace avmplus {");
        s.push("#ifdef VMCFG_SELFTEST");
        for ( var i=0 ; i < selftests.length ; i++ ) {
            var st = selftests[i];
            if (st.ifdef_text != null)
                s.push(st.ifdef_text);
            if (st.ifndef_text != null)
                s.push(st.ifndef_text);
            s.push("namespace "+st.namespace_name+" {");
            s.push("extern void create_" + st.component + "_" + st.category + "(AvmCore* core);");
            s.push("}");
            if (st.ifndef_text != null)
                s.push("#endif");
            if (st.ifdef_text != null)
                s.push("#endif");
        }
        s.push("void SelftestRunner::createGeneratedSelftestClasses() {");
        for ( var i=0 ; i < selftests.length ; i++ ) {
            var st = selftests[i];
            if (st.ifdef_text != null)
                s.push(st.ifdef_text);
            if (st.ifndef_text != null)
                s.push(st.ifndef_text);
            s.push(""+st.namespace_name+"::create_" + st.component + "_" + st.category + "(core);");
            if (st.ifndef_text != null)
                s.push("#endif");
            if (st.ifdef_text != null)
                s.push("#endif");
        }
        s.push("}");
        s.push("#endif // VMCFG_SELFTEST");
        s.push("}");
        return s.join("\n") + "\n";
    }

    var selftests = [];

    function process(input) {
        var st = parse(File.read(input).split("\n"), input);
        selftests.push(st);
        return formatSelftest(input, st);
    }

    var args = System.argv;
    args = args.sort();

    var processed = [];
    for ( var i=0 ; i < args.length ; i++ ) {
        if (!args[i].match(/\.st$/)) {
            print("WARNING: ignoring non-selftest file " + args[i]);
            processed[i] = "";
        } else {
            processed[i] = process(args[i]);
        }
    }

    var prefix_note = "// Generated from " + args.join(", ") + "\n";
    File.write("SelftestExec.cpp", prefix_note + processed.join("\n") + "\n");

    File.write("SelftestInit.cpp", formatGeneratedInitializer());
}

